大众点评业务系统重构总结
(点击上方公众号,可快速关注)
来源:伯乐在线专栏作者 - 陈景广
链接:http://blog.jobbole.com/100328/
之前在《服务化设计模式实践》里面介绍了交易侧系统服务变迁的模式,服务的变迁更好的支持了业务的发展,伴随着业务的发展,对业务系统内部的要求也更好,需要具有更好的扩展性。随着业务的不断发展,每个服务内部的逻辑也变得越来越多,需要有更好的抽象来支持以后更多的业务类型。
1. 项目业务背景
重构的项目有订单服务,预订系统,退款系统;这三个系统都是与用户交易行为息息相关。
其中订单系统参与重构的模块为订单创建,订单状态流转,订单支付;
预订系统的重构主要为了支撑更多的预订方式,如之前已经支持的库存模式、商家接单模式和售中客服模式,伴随着重构还需要支持商家系统直连模式,而且需要能够支持以后业务发展更多的预订模式。
退款服务的复杂度主要来源于多种退款类型,如用户退款,系统退款,商家退款和客服退款等多种类型,而每种类型又有各种不同的退款规则;退款服务需要支持多种业务,如已有的KTV预订和将要扩展出的酒水点单。
在这里我们主要来讲讲预订系统重构,因为这个系统的重构几乎涵盖了订单服务和退款服务重构所使用到的技术
目标
抽象预订流程,并模板化
对可变化的部分支持配置化
在上线过程中支持新老流程切换
2. 业务抽象
由图中可以看出,业务流程非常复杂,一个订单的预订过程会根据不同的情况走不同的预订渠道,如果一个预订渠道因为某种原因预订失败了,可能会继续使用另外一个预订渠道继续进行预订,也就是会发生流转。
另外,在预订成功和预订失败时,会需要做一些其他操作,例如发送短信告知用户结果等;
图中还有一点没有体现的是,在开始发起预订时,需要校验数据的正确性,校验是否复核预订规则等等校验。
根据这些条件我们做了以下抽象:
首先订单从预订开始、预订中到预订成功/失败定义为预订的主流程,其中每个接单都是一个重要的业务节点,这种主流程定义为一级业务。
对于不同的预订模式(如库存模式、商家接单模式、客服售中介入模式和商家系统直连等),抽象为预订渠道。预订渠道之前的转化定义为渠道流转。
预订渠道会直接影响预订结果
预订中、预订成功/失败 时渠道需要个性化的操作,如商家接单渠道开始时需要通知商家等,这种流程会影响一级业务,但其业务具有个性化特征,因此定义为二级业务。
同时预订中、预订成功/失败后 需要进行不影响业务流程的操作,如发送短信告知用户预订结果,记录一些属性等等。这部分业务定义为三级业务。
一级业务是系统最重要的业务,业务流程标准化且会直接影响业务结果;二级业务是一级业务一个步骤,但因为预订渠道的不同而有个性化操作;三级业务是根据业务结果来执行的操作,不会再影响系统的主流程。
3. 重构
3.1 核心业务流程
预订中核心业务流程是最重要的部分,也就是图中所标注的一级业务,每一个步骤都是一个重要的业务节点,且每一个节点都会有一些复杂的逻辑。
因此在重构时,将核心业务流程的实现定义为一个模板引擎,在这个模板引擎中的每一个节点都可以是一个接口,可以任意的配置。在代码上的表现就会是这样的。
开始预订:
public class KtvReserveService {
public KtvReserveResultDTO reserve(KtvReserveContext reserveContext) throws ReserveException {
// 校验
KtvValidateResult validateResult = this.ktvReserveValidateStack.validate(reserveContext);
if (validateResult == null || !validateResult.isValid()) {
return KtvReserveResultDTO.createFailedResult("validate invalid");
}
//判定预订渠道
KtvReserveChannel reserveChannel = reserveChannelJudgeService.judgeChannelType(reserveContext);
reserveDataService.store()
reserveDataService.transferReserveChannelStatus();
//开始渠道预订
ChannelResult channelResult = this.reserveChannelService.reserve(reserveContext);
return KtvReserveResultDTO
.genResult(channelResult.isSuccess(), channelResult.getDesc(), reserveFlow.getReserveId());
}
}
渠道反馈预订结果:
public class KtvReplyReserveService {
@Override
public ReplyReserveResult reply(KtvReplyReserveInfo replyReserveInfo) throws ReplyReserveException {
//校验
KtvValidateResult validateResult = replyReserveValidateStack.validate(replyReserveInfo);
if (validateResult == null || !validateResult.isValid()) {
logger.warn(String.format(" %s validate failed", param));
return ReplyReserveResult.createFailedResult("validate failed");
}
//更新预订状态
reserveDataService.transferReserveChannelStatus();
ReplyReserveResult result;
//判定预订结果
KtvReserveStatus toReserveStatus = this.reserveChannelJudgeService.judgeReserveResult(replyReserveInfo);
boolean reserveFailed =
toReserveStatus == null || toReserveStatus == KtvReserveStatus.ReserveFailed || toReserveStatus == KtvReserveStatus.Init;
if (reserveFailed) {
//预订失败处理
result = this.reserveFailed(replyReserveInfo);
} else if (toReserveStatus == KtvReserveStatus.ReserveSuccess) {
//预订成功处理
result = this.reserveSuccess(replyReserveInfo);
} else {
// 需要转移其他渠道预订
result = ktvReserveTransferService.transferChannel(ktvReserveContext);
}
// 渠道处理内部事务
this.replyReserveChannelService.reply(replyReserveInfo);
return result;
}
}
3.2 校验栈
在业务性很强的服务来说,在业务开始之前需要有复杂的校验,如果在这个服务中支持多种业务类型,还需要根据不同的业务类型来选择不同的校验逻辑,因此在服务中将校验栈独立出来。
校验栈的组装采用责任链模式,这样每个校验service通过组装的方式即可以灵活支持多种校验。但是对于业务主流程来说,把校验service的组装服务并不适合放在主业务流程里,因此在重构的时候将校验栈的组装逻辑放在一个单独的service中采用代理模式进行组装。
public interface KtvReserveValidateService {
/**
* 校验预订信息
* @param reserveContext
* @return
*/
KtvValidateResult validate(KtvReserveContext reserveContext);
}
public class KtvReserveValidateStack implements KtvReserveValidateService {
private List<KtvReserveValidateService> validateServices;
public void setValidateServices(
List<KtvReserveValidateService> validateServices) {
this.validateServices = validateServices;
}
@Override
public KtvValidateResult validate(KtvReserveContext reserveContext) {
if (CollectionUtils.isEmpty(validateServices)) return KtvValidateResult.validResut();
for (KtvReserveValidateService service : validateServices) {
KtvValidateResult result = service.validate(reserveContext);
if (result == null || !result.isValid()) return result;
}
return KtvValidateResult.validResut();
}
}
3.3 业务分级
在前面讲到,在重构中将代码功能分成了一级功能,二级功能和三级功能。
其中一级功能的每个步骤都需要严格保证,如果发生问题就需要直接影响业务流程,例如在预订业务中,预订数据状态的更新就是一级业务,如果更新失败就需要终结业务;
二级业务也是重要的业务,但是不需要二级业务不能影响最终的业务结果,但是当二级业务出错时也需要及时处理,如在更新订单状态为购买成功时发生错误,需要及时告警,或者异步化保证数据一致性;
三级业务完全不影响业务流程,很多都是异步化调用外部服务,如短信通知用户、双写订单上的预订状态(老业务)等。预订服务中的三级业务都是根据预订结果而触发,因此在这里使用观察者模式实现即可。
业务服务对业务流程相对较多,而且每一步出现问题都有可能直接影响购买结果,这种与钱息息相关的业务,一单出错就会有各方来追杀,而且也极大影响用户和商家的体验。对业务分级是将不用影响最终结果的业务剥离出去,将最核心的业务重点对待,不同级别用不同的处理方式。
3.4 数据模型统一
这里的业务模型是业务流程的数据统一。例如在开始预订的业务中使用ReserveContext作为整个业务流程的数据协议,在分业务时也能采用相同的数据,即避免相同数据的重复读取也便于立减和时间。
一个业务流程的处理,其实也是一种服务的处理过程,而数据模型就是其业务的协议,好协议才能产生好实践。
3.5 机制同策略分离
机制同策略分离是Unix设计中的基本原则之一,是将将程序的引擎(程序核心域的核心算法和逻辑规格)从接口部分(接受用户命令,展示结果等)分离;因为在一个系统中策略变化相对较多,例如预订服务的三级业务中以后并不需要再同步订单上的预订状态,如果策略的变化影响到机制会使得系统很不稳定,有需求修改时会导致系统大的修改,在功能上线需要QA验证的范围也会很大;导致策略变得死板,难以适应用户需求的改变,任何策略的改变都极有可能动摇机制。
机制同策略分离的机制引用最广泛的是MVC模式。
在这里预订流程的基本模型就是我们的引擎,在引擎中规定了几个基本的业务节点,而每个业务点的实现都有各自的接口规定,如果有需求的变更只需要更改各个业务节点自身的接口实现即可。至于如何接收支付结果发起预订,以及何种情况下反馈预订接口都是与核心流程分离的。
4. 如何上线
重构最痛苦的部分是怎么把项目上线。
在这几次的重构中,主要实行了两种重构:
项目内部逻辑重构,但没有新建数据表,对外的接口没有修改;
修改了对外的接口,新增了数据表。
第一种模式重构,在上线时比较容易,因为基本不用考虑到新老逻辑兼容的问题,第二种模式的重构在上线时需要考虑新老接口的兼容。在这次预订服务重构过程中,修改了对外接口新增了数据记录,而且重构后的系统逻辑也与新的数据表耦合,因此在新老接口上需要做特别的兼容。这次预订服务改造主要涉及到发起预订和预订反馈,因此在兼容上需要在新老逻辑的入口上都需要做数据转换。另外在测试阶段需要模拟上线的步骤,校验上线每个阶段的新老接口兼容如何,功能是否正常。
分拆上线
一般重构的部分不宜过大,过大时需要考虑的兼容就更多,影响到的外界系统也会更多;一般重构最好的方法是分步重构,重构一部分之后验证上线,小步快跑的方式上线。
5. 总结
在这个重构的过程中我们主要有一下基本的原则:
机制同策略分离
协议统一化和简单化
开闭原则
主要使用到一下设计模式:
代理模式
监听者模式
责任链模式
装饰者模式
专栏作者简介 ( 点击 → 加入专栏作者 )
陈景广:自2013.05工作于大众点评至今
打赏支持作者写出更多好文章,谢谢!
【今日微信公号推荐↓】
更多推荐请看《值得关注的技术和设计公众号》
其中推荐了包括技术、设计、极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!